\section{调试及性能分析}
\subsection{\texttt{journalctl}使用}
\begin{questionbox}
使用 Linux 上的 journalctl 或 macOS 上的 log show 命令来获取最近一天中超级用户的登录信息及其所执行的指令。如果找不到相关信息，您可以执行一些无害的命令，例如 sudo ls 然后再次查看。
\end{questionbox}

\texttt{Linux}使用\texttt{systemd}日志系统，我们可以使用\texttt{journalctl}来查看日志，具体命令如下：
\begin{lstlisting}
sudo journalctl --since "24 hours ago" | grep -i sudo
\end{lstlisting}

我们来分析一下命令的作用，\texttt{journalctl}是日志系统查看工具，后面的\texttt{--since "24 hours ago"}则是一个时间过滤选项，我们同样可以替换""中的内容，例如\texttt{--since "5 min ago"}以及具体的时间节点\texttt{--since "2025-05-05 00:00:00"}。后半部分的\texttt{grep -i sudo}则是忽略大小写在输入中查找\texttt{sudo}的字符串。

我们同样可以主动的运行一个无害的\texttt{sudo}命令：
\begin{lstlisting}
sudo ls >/dev/null 2>&1
\end{lstlisting}
然后查看日志信息：
\begin{lstlisting}
sudo journalctl --since "5 min ago" | grep -i "sudo\|ls"
\end{lstlisting}

具体操作和结果如图\ref{journalctl查看系统日志}
\begin{figure}[htb]
	\centering
	\includegraphics[width=0.8\linewidth]{assets/images/屏幕截图 2025-09-19 112333.png}
	\caption{journalctl查看系统日志}
	\label{journalctl查看系统日志}
\end{figure}

\subsection{pdb实践教程}
\begin{lstlisting}
学习 这份 pdb 实践教程并熟悉相关的命令。更深入的信息您可以参考 这份 教程。
\end{lstlisting}
教程实践过程如图\ref{pdb实践图片1},图\ref{pdb实践图片2},图\ref{pdb实践图片3}
\begin{figure}[H]
	\centering
	\includegraphics[width=0.8\linewidth]{assets/images/pdb2.png}
	\caption{pdb实践图片1}
	\label{pdb实践图片1}
\end{figure}
\begin{figure}[H]
	\centering
	\includegraphics[width=\linewidth]{assets/images/pdb1.png}
	\caption{pdb实践图片2}
	\label{pdb实践图片2}
\end{figure}
\begin{figure}[H]
	\centering
	\includegraphics[width=0.8\linewidth]{assets/images/image.png}
	\caption{pdb实践图片3}
	\label{pdb实践图片3}
\end{figure}

实践教程内容过多这里就不多赘述，仅展示练习结果。下面事主要命令：
\begin{enumerate}
    \item l(ist) - 展示当前行附近的 11 行，或者是继续之前的展示。
    \item s(tep) - 执行当前行，会停在第一个可能停下的地方。
    \item n(ext) - 继续执行程序到当前函数的下一行或者是到它返回结果。
    \item b(reak) - 设置程序断点 (取决于提供的参数)。
    \item r(eturn) - 继续执行到当前函数返回结果
\end{enumerate}

\subsection{shellcheckd对脚本进行检查}
\begin{questionbox}
    安装 shellcheck 并尝试对下面的脚本进行检查。这段代码有什么问题吗？请修复相关问题。在您的编辑器中安装一个 linter 插件，这样它就可以自动地显示相关警告信息。
\begin{lstlisting}
 #!/bin/sh
## Example: a typical script with several problems
for f in $(ls *.m3u)
do
  grep -qi hq.*mp3 $f \
    && echo -e 'Playlist $f contains a HQ file in mp3 format'
done
\end{lstlisting}
\end{questionbox}

我们观察脚本会发现下面的这些问题：
\begin{enumerate}
\item 直接将\texttt{\$(ls *.m3u)}的结果作为循环变量事不安全的，特别是处理包含特殊字符或空格的文件名时。
\item 在\texttt{grep}命令中，\texttt{\$f}和\texttt{hq.*mp3}没有被正确引用，这可能导致在文件名搜索模式包含空格或其他特殊字符时出现问题。
\end{enumerate}
我们修复代码如下：
\begin{lstlisting}
#!/bin/sh
## Example: a typical script with several problems fixed
for f in *.m3u
do
    if grep -qi "hq.*mp3" "$f"; then
        echo "Playlist '$f' contains a HQ file in mp3 format"
    fi
done
\end{lstlisting}

然后我们安装\texttt{shellcheck}，在bash中输入如下命令：
\begin{lstlisting}
    sudo apt-get install shellcheck
\end{lstlisting}

 操作和结果如图\ref{shellcheck操作及结果}
 \begin{figure}[htb]
	\centering
	\includegraphics[width=\linewidth]{assets/images/shellcheck.png}
	\caption{shellcheck操作及结果}
	\label{shellcheck操作及结果}
\end{figure}

\subsection{\texttt{cProfile}和\texttt{line\_profile}}
\begin{questionbox}
。请使用 cProfile 和 line\_profiler 来比较插入排序和快速排序的性能。两种算法的瓶颈分别在哪里？然后使用 memory\_profiler 来检查内存消耗，为什么插入排序更好一些？然后再看看原地排序版本的快排。附加题：使用 perf 来查看不同算法的循环次数及缓存命中及丢失情况。
\end{questionbox}

为了解决这个问题我们先写一个包含插入排序和快速排序实现的Python文件，这个文件名字叫作\texttt{sort\_algorithms.py}，代码如下：
\begin{lstlisting}
# sort_algorithms.py

def insertion_sort(arr):
    for i in range(1, len(arr)):
        key = arr[i]
        j = i - 1
        while j >= 0 and key < arr[j]:
            arr[j + 1] = arr[j]
            j -= 1
        arr[j + 1] = key
    return arr

def quick_sort(arr):
    if len(arr) <= 1:
        return arr
    pivot = arr[len(arr) // 2]
    left = [x for x in arr if x < pivot]
    middle = [x for x in arr if x == pivot]
    right = [x for x in arr if x > pivot]
    return quick_sort(left) + middle + quick_sort(right)
\end{lstlisting}

同时我们也需要安装\texttt{line\_profile}和\texttt{memory\_profiler}两个库，在命令行中输入如下命令：
\begin{lstlisting}
pip install line_profiler memory_profiler
\end{lstlisting}

做好上面的准备工作，接下来正式开始我们的实验。创建一个新的Python文件\texttt{profile\_sort.py}，并使用\texttt{cProfile}来分析两个排序算法的性能:
\begin{lstlisting}
import cProfile
from sort_algorithms import insertion_sort, quick_sort
import random

# 生成随机数组
arr = [random.randint(0, 1000) for _ in range(1000)]

# 使用 cProfile 分析插入排序
print("Profiling Insertion Sort:")
cProfile.run('insertion_sort(arr.copy())')

# 使用 cProfile 分析快速排序
print("Profiling Quick Sort:")
cProfile.run('quick_sort(arr.copy())')
\end{lstlisting}
写好保存然后我们运行它，这个时候我们就可以看见每个函数的时间消耗。

当然，为了更好的了解每一行代码的执行情况，这个时候我们就要用到\texttt{line\_profiler}了。我们首先对\texttt{sort\_algorithms.py}进行一些修改：
\begin{lstlisting}
# sort_algorithms.py

@profile
def insertion_sort(arr):
    for i in range(1, len(arr)):
        key = arr[i]
        j = i - 1
        while j >= 0 and key < arr[j]:
            arr[j + 1] = arr[j]
            j -= 1
        arr[j + 1] = key
    return arr

@profile
def quick_sort(arr):
    if len(arr) <= 1:
        return arr
    pivot = arr[len(arr) // 2]
    left = [x for x in arr if x < pivot]
    middle = [x for x in arr if x == pivot]
    right = [x for x in arr if x > pivot]
    return quick_sort(left) + middle + quick_sort(right)
\end{lstlisting}

接着在命令行中输入：
\begin{lstlisting}
kernprof -l -v sort_algorithms.py
\end{lstlisting}
这将会输出每行代码的执行时间和调用次数。

接着在这个文件中添加\texttt{@profile}装饰器，然后在命令行中输入如下命令：
\begin{lstlisting}
python -m memory_profiler sort_algorithms.py
\end{lstlisting}
这个命令将会显示程序运行时的内存使用情况。具体的操作及结果如图\ref{cProfile和line_profiler结果}
\begin{figure}[H]
	\centering
	\includegraphics[width=0.7\linewidth]{assets/images/cProfile和line_profiler结果.png}
	\caption{cProfile和line\_profiler结果}
	\label{cProfile和line_profiler结果}
\end{figure}

\subsection{\texttt{pycallgraph}和\texttt{graphviz}}
\begin{questionbox}
这里有一些用于计算斐波那契数列 Python 代码，它为计算每个数字都定义了一个函数：
\begin{lstlisting}
#!/usr/bin/env python
def fib0(): return 0

def fib1(): return 1

s = """def fib{}(): return fib{}() + fib{}()"""

if __name__ == '__main__':

    for n in range(2, 10):
        exec(s.format(n, n-1, n-2))
    # from functools import lru_cache
    # for n in range(10):
    #     exec("fib{} = lru_cache(1)(fib{})".format(n, n))
    print(eval("fib9()"))
\end{lstlisting}
将代码拷贝到文件中使其变为一个可执行的程序。首先安装 pycallgraph 和 graphviz(如果您能够执行 dot, 则说明已经安装了 GraphViz.)。并使用 pycallgraph graphviz -- ./fib.py 来执行代码并查看 pycallgraph.png 这个文件。fib0 被调用了多少次？我们可以通过记忆法来对其进行优化。将注释掉的部分放开，然后重新生成图片。这回每个 fibN 函数被调用了多少次？
\end{questionbox}

我们首先安装\texttt{pycallgraph}和\texttt{graphviz},在命令行中输入如下命令：
\begin{lstlisting}
pip install pycallgraph graphviz
sudo apt-get install graphviz
\end{lstlisting}
然后我们将下面的代码保存到\texttt{fib.py}：
\begin{lstlisting}
#!/usr/bin/env python

def fib0(): return 0

def fib1(): return 1

s = """def fib{}(): return fib{}() + fib{}()"""

if __name__ == '__main__':
    for n in range(2, 10):
        exec(s.format(n, n-1, n-2))
    # from functools import lru_cache
    # for n in range(10):
    #     exec("fib{} = lru_cache(1)(fib{})".format(n, n))
    print(eval("fib9()"))
\end{lstlisting}
然后运行
\begin{lstlisting}
pycallgraph graphviz -- ./fib.py
\end{lstlisting}

结果和操作如图\ref{第一次fib.py},图\ref{第一次的生成图}
\begin{figure}[H]
	\centering
	\includegraphics[width=\linewidth]{assets/images/屏幕截图 2025-09-21 210846.png}
	\caption{第一次fib.py}
	\label{第一次fib.py}
\end{figure}

\begin{figure}[H]
	\centering
	\includegraphics[width=0.6\linewidth]{assets/images/pycallgraph1.png}
	\caption{第一次的生成图}
	\label{第一次的生成图}
\end{figure}

我们可以发现函数的调用次数为34

然后我们修改\texttt{fib.py}，去掉中间注释的部分，代码如下：
\begin{lstlisting}
def fib0(): return 0

def fib1(): return 1

s = """def fib{}(): return fib{}() + fib{}()"""

if __name__ == '__main__':
    for n in range(2, 10):
        exec(s.format(n, n-1, n-2))
    from functools import lru_cache
    for n in range(10):
        exec("fib{} = lru_cache(1)(fib{})".format(n, n))
    print(eval("fib9()"))
\end{lstlisting}

具体操作和结果如图\ref{第二次的fib},图\ref{第二次的生成图}
\begin{figure}[H]
	\centering
	\includegraphics[width=\linewidth]{assets/images/屏幕截图 2025-09-21 211423.png}
	\caption{第二次的fib}
	\label{第二次的fib}
\end{figure}

\begin{figure}[H]
	\centering
	\includegraphics[width=0.8\linewidth]{assets/images/pycallgraph.png}
	\caption{第二次的生成图}
	\label{第二次的生成图}
\end{figure}

我们发现这里的调用次数还是34，但是我们对比图\ref{第一次的生成图}和图\ref{第二次的生成图}我们可以发现图\ref{第二次的生成图}明显短了很多，这表明函数被优化了，调用次数减少了。

\subsection{监听端口并停止}
\begin{questionbox}
我们经常会遇到的情况是某个我们希望去监听的端口已经被其他进程占用了。让我们通过进程的 PID 查找相应的进程。首先执行 python -m http.server 4444 启动一个最简单的 web 服务器来监听 4444 端口。在另外一个终端中，执行 lsof | grep LISTEN 打印出所有监听端口的进程及相应的端口。找到对应的 PID 然后使用 kill <PID> 停止该进程。
\end{questionbox}

要解决这个问题，我们可以按照下面的步骤进行操作：

首先，我们在终端1中启动一个Python内置的HTTP服务器来监听4444端口：
\begin{lstlisting}
python -m http.server 4444
\end{lstlisting}

接着我们在终端2中使用\texttt{lsof}命令查找所有正在监听端口的进程：
\begin{lstlisting}
lsof -i :4444
\end{lstlisting}
最后我们找到PID并使用\texttt{kill}命令终止：
\begin{lstlisting}
kill <PID>
\end{lstlisting}

具体结果和过程如图\ref{监听端口并终止}
\begin{figure}[H]
	\centering
	\includegraphics[width=\linewidth]{assets/images/屏幕截图 2025-09-21 222104.png} \\[2ex]
	\includegraphics[width=\linewidth]{assets/images/屏幕截图 2025-09-21 222145.png}
	\caption{监听端口并终止}
	\label{监听端口并终止}
\end{figure}